import gmaps
from worker import WorkerSingle, WorkerParallel
from threading import Thread, Lock
from PIL import Image


class AddressFinder(WorkerSingle):
    
    """Background worker for gmaps.getLocation."""

    def __init__(self, key, address, block=128):
        WorkerSingle.__init__(self, block)
        self.key = key
        self.address = address

    def work(self):
        self.result = gmaps.getLocation(self.key, self.address,
            self.set_progress, self.block)


class PanoramaIDFinder(WorkerSingle):
    
    """Background worker for gmaps.getNearestPanoramaID."""

    def __init__(self, latlng, block=128):
        WorkerSingle.__init__(self, block)
        self.latlng = latlng

    def work(self):
        self.result = gmaps.getNearestPanoramaID(self.latlng,
            self.set_progress, self.block)


class PanoramaDetailsFinder(WorkerSingle):
    
    """Background worker for gmaps.getPanoramaDetails."""

    def __init__(self, panoId, block=128):
        WorkerSingle.__init__(self, block)
        self.panoId = panoId

    def work(self):
        self.result = gmaps.getPanoramaDetails(self.panoId,
            self.set_progress, self.block)


class PanoramaDownloader(WorkerParallel):

    """Downloads panoramas from Google Maps tile by tile."""
    
    def __init__(self, details, zoom, block=1024, parallel=4):
        """Constructs a PanoramaDownloader.

        Index for parallel downloads is (x, y).

        Parameters:
        details -- PanoramaDetails describing StreetView location.
        zoom -- Panorama resolution to download. Must be in valid range.
        block -- Chunk of bytes to download before updating progress.
        parallel -- Maximum number of concurrent downloads.

        """
        WorkerParallel.__init__(self, block, parallel)
        # Save parameters.
        self.details = details
        self.zoom = zoom
        # Ensure zoom is OK.
        if zoom not in details.zoomRange():
            raise Exception("Zoom %s not in valid range (0-%s)" %
                (zoom, details.maxZoom))
        # Initialise 2D lists.
        self.cols, self.rows = self.details.tilesAtZoom(zoom)
        self.tilesLock = Lock()
        self.tiles = []
#        self.progress = []
        self.progress = {}
        for x in range(self.cols):
            self.tiles.append([])
#            self.progress.append([])
            for y in range(self.rows):
                self.tiles[-1].append(None)
                self.progress[(x, y)] = None
#                self.progress[-1].append(None)
        # How do we distribute parallel downloads evenly?
        self.distribution = distribute(self.cols, parallel)
        # Status variables.
        self.running = False
        self.downloaded = False

    def work(self):
        """Download the tiles."""
        print "DEBUG: start work()"
        self.running = True
        # Initiate parallel downloads.
        for y in range(self.rows):
            x = 0
            for n in self.distribution:
                threads = []
                for i in range(n):
                    thread = Thread(
                        target = self.downloadTile, args=(x, y),
                        name="Pano_%i_%i" % (x, y))
                    thread.start()
                    threads.append(thread)
                    x += 1
                # Block main thread until completed.
                for thread in threads:
                    thread.join()
        # Finished downloading.
        self.downloaded = True
        # Build complete image.
        image = Image.new("RGB", self.details.sizeAtZoom(self.zoom), "black")
        for y in range(self.rows):
            for x in range(self.cols):
                try:
                    tile = self.tiles[x][y]
                except KeyError:
                    raise Exception("Failed to download all tiles")
                image.paste(tile,
                    (self.details.tileWidth * x,
                     self.details.tileHeight * y))
        # All finished!
        self.running = False
        self.result = image
    
    def downloadTile(self, x, y):
        """Downloads tile at (x, y). Internal use only."""
        print "DEBUG: start downloadTile(%i, %i)" % (x, y)
        # Progress callback function.
        def set_tile_progress(read, size):
            self.set_progress((x, y), read, size)
        # Download image.
        image = gmaps.getPanoramaTile(self.details.id, self.zoom,
            x, y, set_tile_progress, self.block)
        # Store image.
        self.tilesLock.acquire()
        self.tiles[x][y] = image
        self.tilesLock.release()


class PanoramaDownloaderSimple(WorkerParallel):

    """Downloads panoramas from Google Maps tile by tile."""
    
    def __init__(self, details, zoom, block=1024):
        """Constructs a PanoramaDownloader.

        Index for parallel downloads is (x, y).

        Parameters:
        details -- PanoramaDetails describing StreetView location.
        zoom -- Panorama resolution to download. Must be in valid range.
        block -- Chunk of bytes to download before updating progress.

        """
        WorkerParallel.__init__(self, block, 1024)
        # Save parameters.
        self.details = details
        self.zoom = zoom
        # Ensure zoom is OK.
        if zoom not in details.zoomRange():
            raise Exception("Zoom %i not in valid range (0-%i)" %
                (zoom, details.maxZoom))
        # Initialise 2D lists.
        self.rows, self.cols = self.details.tilesAtZoom(zoom)
        self.tiles = []
        self.progress = {}
        for x in range(self.cols):
            self.tiles.append([])
            for y in range(self.rows):
                self.tiles[-1].append(None)
                self.progress[(x, y)] = None
        # Status variables.
        self.running = False
        self.downloaded = False

    def work(self):
        """Download the tiles."""
        self.running = True
        # Initiate parallel downloads.
        for y in range(self.rows):
            for x in range(self.cols):
                self.downloadTile(x, y)
        # Finished downloading.
        self.downloaded = True
        # Build complete image.
        image = Image.new("RGB", self.details.sizeAtZoom(self.zoom), "black")
        for y in range(self.rows):
            for x in range(self.cols):
                try:
                    tile = self.tiles[x][y]
                except KeyError:
                    raise Exception("Failed to download all tiles")
                image.paste(tile,
                    (self.details.tileWidth * x,
                     self.details.tileHeight * y))
        # All finished!
        self.running = False
        self.result = image
    
    def downloadTile(self, x, y):
        """Downloads tile at (x, y). Internal use only."""
        print "DEBUG: start downloadTile(%i, %i)" % (x, y)
        # Progress callback function.
        def set_tile_progress(read, size):
            self.set_progress((x, y), read, size)
        # TODO This is a bit of a hack to get progress bar to reset earlier.
        set_tile_progress(0, 1)
        # Download image.
        image = gmaps.getPanoramaTile(self.details.id, self.zoom,
            x, y, set_tile_progress, self.block)
        self.tiles[x][y] = image


#    def tiles(self):
#        """Gets the layout of tiles.
#
#        Returns:
#        A tuple of the number of tiles in x, y directions, or None if the
#        download hasn't started yet.
#
#        """
#        if not self.running:
#            return None
#        return (self.cols, self.rows)

#    def progress(self, x, y):
#        """Gets the progress of tile download (x, y).
#        
#        Returns:
#        A tuple (bytes downloaded, total bytes), or None if that download
#        hasn't begun yet.
#
#        Throws:
#        IndexError if the tile indices are out of range.
#
#        """
#        if not self.running:
#            return None
#        if x < 0 or x >= self.cols or y < 0 or y >= self.rows:
#            raise IndexError("Tile (%s, %s) out of bounds (%s, %s)" %
#                (x, y, self.cols, self.rows)
#        # Threaded access.
#        self.progressLock.acquire()
#        progress = self.progress[x][y]
#        self.progressLock.release()
#        return progress



def distribute(total, size):
    """Distributes [total] elements evenly in groups of up to [size]."""
    if total <= 0 or size <= 0:
        return None
    groups = (total + size - 1) / size
    dist = [0] * groups
    i = 0
    while i < total:
        dist[i % groups] += 1
        i += 1
    return dist

